package com.tju.shortlink.project.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.text.StrBuilder;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.google.common.collect.Lists;
import com.tju.shortlink.project.common.biz.user.UserContext;
import com.tju.shortlink.project.common.constant.MQConstans;
import com.tju.shortlink.project.common.convention.exception.ClientException;
import com.tju.shortlink.project.common.convention.exception.ServiceException;
import com.tju.shortlink.project.common.enums.ValidDateTypeEnum;
import com.tju.shortlink.project.config.GotoDomainWhiteListConfiguration;
import com.tju.shortlink.project.dao.entity.*;
import com.tju.shortlink.project.dao.mapper.*;
import com.tju.shortlink.project.dto.biz.ShortLinkStatsRecordDTO;
import com.tju.shortlink.project.dto.req.*;
import com.tju.shortlink.project.dto.resp.*;
import com.tju.shortlink.project.mq.proceducer.RabbitMqProducer;
import com.tju.shortlink.project.mq.proceducer.ShortLinkStatsSaveProducer;
import com.tju.shortlink.project.service.ILinkService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.tju.shortlink.project.toolkit.HashUtil;
import com.tju.shortlink.project.toolkit.LinkUtil;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import cn.hutool.core.lang.UUID;
import static com.tju.shortlink.project.common.constant.RedisKeyConstant.*;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author mxl
 * @since 2024-07-06
 */
@Service
@Slf4j
@RequiredArgsConstructor
public class LinkServiceImpl extends ServiceImpl<LinkMapper, LinkDo> implements ILinkService {

    private final RBloomFilter<String> shortUriCreateCachePenetrationBloomFilter;

    private final LinkGotoMapper linkGotoMapper;

    private final StringRedisTemplate redisTemplate;

    private final RedissonClient redissonClient;

    private final LinkGroupMapper linkGroupMapper;

    private final GotoDomainWhiteListConfiguration gotoDomainWhiteListConfiguration;

    private final Executor getFaviconExecutor;

    private final ShortLinkStatsSaveProducer shortLinkStatsSaveProducer;

    private final RabbitMqProducer rabbitMqProducer;

    private static final String UIP_UV_LUA_SCRIPT_PATH = "lua/uip_uv_hyperloglog.lua";

    @Value("${short-link.domain.default}")
    private String createShortLinkDefaultDomain;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public ShortLinkCreateRespDTO createShortLink(ShortLinkCreateReqDTO requestParam) {
        // 1. 验证网址白名单
        verificationWhitelist(requestParam.getOriginUrl());
        String host = null;
        try {
            URL baseUrl = new URL(requestParam.getOriginUrl());
            host = baseUrl.getHost();
        } catch (MalformedURLException e) {
            throw new ClientException("网址不正确");
        }

        // 2. 生成短链接后缀 并封装
        String shortLinkSuffix = generateSuffix(requestParam);
        LinkDo linkDo = BeanUtil.toBean(requestParam, LinkDo.class);
        linkDo.setTotalPv(0);
        linkDo.setTotalUip(0);
        linkDo.setTotalUv(0);
        linkDo.setDomain(createShortLinkDefaultDomain);
        String fullShortUrl = StrBuilder.create(createShortLinkDefaultDomain).append("/").append(shortLinkSuffix).toString();
        linkDo.setFullShortUrl(fullShortUrl);
        linkDo.setEnableStatus(0);
        linkDo.setShortUri(shortLinkSuffix);
        linkDo.setFavicon(host);
        // 2.1 异步获取 Favicon 减小等待io的时间
        CompletableFuture.runAsync(() -> {
            getFavicon(requestParam.getOriginUrl());
        }, getFaviconExecutor);

        // 3. 建立路由表方便后续通过URL访问到响应的短链接数据
        LinkGotoDo linkGotoDo = LinkGotoDo.builder().fullShortUrl(fullShortUrl).gid(requestParam.getGid()).build();

        // 4. 操作DB
        try{
            baseMapper.insert(linkDo);
            linkGotoMapper.insert(linkGotoDo);
        }catch (DuplicateKeyException e){
            if (!shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl)) {
                shortUriCreateCachePenetrationBloomFilter.add(fullShortUrl);
            }
            log.warn("短链接：{}， 重复入库", fullShortUrl);
            throw new ClientException("短链接生成失败，请稍后再试");
        }
        // 5. 缓存预热
        redisTemplate.opsForValue().set(
                String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
                requestParam.getOriginUrl(),
                LinkUtil.getLinkCacheValidDate(requestParam.getValidDate()),
                TimeUnit.MICROSECONDS);
        shortUriCreateCachePenetrationBloomFilter.add(fullShortUrl);

        // 5. 封装返回
        return ShortLinkCreateRespDTO
                .builder()
                .fullShortUrl("http://"+linkDo.getFullShortUrl())
                .gid(requestParam.getGid())
                .originUrl(requestParam.getOriginUrl()).build();
    }

    @Override
    // 分布式锁对比
    public ShortLinkCreateRespDTO createShortLinkByLock(ShortLinkCreateReqDTO requestParam) {
        verificationWhitelist(requestParam.getOriginUrl());
        String host = null;
        try {
            URL baseUrl = new URL(requestParam.getOriginUrl());
            host = baseUrl.getHost();
        } catch (MalformedURLException e) {
            throw new ClientException("网址不正确");
        }
        String fullShortUrl;
        RLock lock = redissonClient.getLock(SHORT_LINK_CREATE_LOCK_KEY);
        lock.lock();
        try {
            String shortLinkSuffix = generateSuffixByLock(requestParam);
            fullShortUrl = StrBuilder.create(createShortLinkDefaultDomain)
                    .append("/")
                    .append(shortLinkSuffix)
                    .toString();
            LinkDo shortLinkDO = LinkDo.builder()
                    .domain(createShortLinkDefaultDomain)
                    .originUrl(requestParam.getOriginUrl())
                    .gid(requestParam.getGid())
                    .createdType(requestParam.getCreatedType())
                    .validDateType(requestParam.getValidDateType())
                    .validDate(requestParam.getValidDate())
                    .describe(requestParam.getDescribe())
                    .shortUri(shortLinkSuffix)
                    .enableStatus(0)
                    .totalPv(0)
                    .totalUv(0)
                    .totalUip(0)
                    .delTime(0L)
                    .fullShortUrl(fullShortUrl)
                    .favicon(host)
                    .build();

            // 2.1 异步获取 Favicon 减小等待io的时间
            CompletableFuture.runAsync(() -> {
                getFavicon(requestParam.getOriginUrl());
            }, getFaviconExecutor);

            LinkGotoDo linkGotoDO = LinkGotoDo.builder()
                    .fullShortUrl(fullShortUrl)
                    .gid(requestParam.getGid())
                    .build();
            try {
                baseMapper.insert(shortLinkDO);
                linkGotoMapper.insert(linkGotoDO);
            } catch (DuplicateKeyException ex) {
                throw new ServiceException(String.format("短链接：%s 生成重复", fullShortUrl));
            }
            redisTemplate.opsForValue().set(
                    String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
                    requestParam.getOriginUrl(),
                    LinkUtil.getLinkCacheValidDate(requestParam.getValidDate()), TimeUnit.MILLISECONDS
            );
        } finally {
            lock.unlock();
        }
        return ShortLinkCreateRespDTO.builder()
                .fullShortUrl("http://" + fullShortUrl)
                .originUrl(requestParam.getOriginUrl())
                .gid(requestParam.getGid())
                .build();
    }

    @Override
    public IPage<ShortLinkPageRespDTO> pageShortLink(ShortLinkPageReqDTO requestParam) {
        // 1. 查询
        IPage<LinkDo> resultPage = baseMapper.pageLink(requestParam);
        // 2. 封装
        return resultPage.convert(each -> {
            ShortLinkPageRespDTO bean = BeanUtil.toBean(each, ShortLinkPageRespDTO.class);
            bean.setDomain("http://"+bean.getDomain());
            if(StrUtil.isNotBlank(bean.getFavicon())){
                bean.setFavicon(redisTemplate.opsForValue().get(SHORT_LINK_FAVICON_KEY+bean.getFavicon()));
            }
            return bean;
        });
    }

    @Override
    public List<ShortLinkGroupCountQueryRespDTO> listGroupShortLinkCount(List<String> gids) {
        // 查询 DB
        QueryWrapper<LinkDo> linkDoQueryWrapper = Wrappers.<LinkDo>query()
                .select("gid, count(*) as shortLinkCount")
                .eq("enable_status", 0)
                .in("gid", gids)
                .groupBy("gid");
        List<Map<String, Object>> shortlinkDoList = baseMapper.selectMaps(linkDoQueryWrapper);

        // 封装
        return BeanUtil.copyToList(shortlinkDoList, ShortLinkGroupCountQueryRespDTO.class);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void updateShortLink(ShortLinkUpdateReqDTO requestParam) {
        // 1. 验证网址白名单
        verificationWhitelist(requestParam.getOriginUrl());

        // 2. 查出对应的记录
        LambdaQueryWrapper<LinkDo> queryWrapper = Wrappers.lambdaQuery(LinkDo.class)
                .eq(LinkDo::getGid, requestParam.getOriginGid())
                .eq(LinkDo::getFullShortUrl, requestParam.getFullShortUrl())
                .eq(LinkDo::getDelFlag, 0)
                .eq(LinkDo::getEnableStatus, 0);
        LinkDo hasLinkDo = baseMapper.selectOne(queryWrapper);
        if (hasLinkDo == null) {
            throw new ClientException("短链接记录不存在");
        }
        String host = null;
        try {
            URL baseUrl = new URL(requestParam.getOriginUrl());
            host = baseUrl.getHost();
        } catch (MalformedURLException e) {
            throw new ClientException("网址不正确");
        }

        // 3. 移动短链接分组
        if (Objects.equals(hasLinkDo.getGid(), requestParam.getGid())) {
            // 3.1 短链接分组未移动直接更新DB
            LambdaUpdateWrapper<LinkDo> updateWrapper = Wrappers.lambdaUpdate(LinkDo.class)
                    .eq(LinkDo::getFullShortUrl, requestParam.getFullShortUrl())
                    .eq(LinkDo::getGid, requestParam.getGid())
                    .eq(LinkDo::getDelFlag, 0)
                    .eq(LinkDo::getEnableStatus, 0)
                    .set(Objects.equals(requestParam.getValidDateType(), ValidDateTypeEnum.PERMANENT.getType()), LinkDo::getValidDate, null);
            LinkDo shortLinkDO = LinkDo.builder()
                    .domain(hasLinkDo.getDomain())
                    .shortUri(hasLinkDo.getShortUri())
                    .createdType(hasLinkDo.getCreatedType())
                    .gid(requestParam.getGid())
                    .originUrl(requestParam.getOriginUrl())
                    .favicon(host)
                    .describe(requestParam.getDescribe())
                    .validDateType(requestParam.getValidDateType())
                    .validDate(requestParam.getValidDate())
                    .build();
            CompletableFuture.runAsync(() -> {
                getFavicon(requestParam.getOriginUrl());
            }, getFaviconExecutor);

            baseMapper.update(shortLinkDO, updateWrapper);
        } else {
            // 3.2 短链接分组移动
            // 3.3 获得分布式写锁 更新gid避免统计信息统计到脏数据
            RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(String.format(LOCK_GID_UPDATE_KEY, requestParam.getFullShortUrl()));
            RLock rLock = readWriteLock.writeLock();
            rLock.lock();
            try {
                // 3.3.1 删除原表数据
                LambdaUpdateWrapper<LinkDo> linkUpdateWrapper = Wrappers.lambdaUpdate(LinkDo.class)
                        .eq(LinkDo::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkDo::getGid, hasLinkDo.getGid())
                        .eq(LinkDo::getDelFlag, 0)
                        .eq(LinkDo::getDelTime, 0L)
                        .eq(LinkDo::getEnableStatus, 0);
                LinkDo delLinkDo = LinkDo.builder()
                        .delTime(System.currentTimeMillis())
                        .build();
                delLinkDo.setDelFlag(1);
                baseMapper.update(delLinkDo, linkUpdateWrapper);

                // 3.3.2 插入新数据
                LinkDo shortLinkDO = LinkDo.builder()
                        .domain(createShortLinkDefaultDomain)
                        .originUrl(requestParam.getOriginUrl())
                        .gid(requestParam.getGid())
                        .createdType(hasLinkDo.getCreatedType())
                        .validDateType(requestParam.getValidDateType())
                        .validDate(requestParam.getValidDate())
                        .describe(requestParam.getDescribe())
                        .shortUri(hasLinkDo.getShortUri())
                        .enableStatus(hasLinkDo.getEnableStatus())
                        .totalPv(hasLinkDo.getTotalPv())
                        .totalUv(hasLinkDo.getTotalUv())
                        .totalUip(hasLinkDo.getTotalUip())
                        .fullShortUrl(hasLinkDo.getFullShortUrl())
                        .favicon(host)
                        .delTime(0L)
                        .build();
                // 3.3.2.1 异步获取 Favicon 减小等待io的时间
                CompletableFuture.runAsync(() -> {
                    getFavicon(requestParam.getOriginUrl());
                }, getFaviconExecutor);

                baseMapper.insert(shortLinkDO);

                // 3.3.3 更新GOTO表
                LambdaQueryWrapper<LinkGotoDo> linkGotoQueryWrapper = Wrappers.lambdaQuery(LinkGotoDo.class)
                        .eq(LinkGotoDo::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkGotoDo::getGid, hasLinkDo.getGid());
                LinkGotoDo shortLinkGotoDO = linkGotoMapper.selectOne(linkGotoQueryWrapper);
                linkGotoMapper.delete(linkGotoQueryWrapper);
                shortLinkGotoDO.setGid(requestParam.getGid());
                linkGotoMapper.insert(shortLinkGotoDO);
            } finally {
                rLock.unlock();
            }
        }
        // 4. 跟过期时间
        if (!Objects.equals(hasLinkDo.getValidDateType(), requestParam.getValidDateType())
                || !Objects.equals(hasLinkDo.getValidDate(), requestParam.getValidDate())
                || !Objects.equals(hasLinkDo.getOriginUrl(), requestParam.getOriginUrl())) {
            // 4.1 过期时间或者原始短链接发生变化 需要更新缓存映射 同时利用canal监听binlog 进行异步的删除缓存
            redisTemplate.delete(String.format(GOTO_SHORT_LINK_KEY, requestParam.getFullShortUrl()));
            Date currentDate = new Date();
            if (hasLinkDo.getValidDate() != null && hasLinkDo.getValidDate().before(currentDate)) {
                // 4.2 如果连接有效 还需要去除空值缓存
                if (Objects.equals(requestParam.getValidDateType(), ValidDateTypeEnum.PERMANENT.getType()) || requestParam.getValidDate().after(currentDate)) {
                    redisTemplate.delete(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, requestParam.getFullShortUrl()));
                }
            }
        }
    }

    @Override
    public void restoreUrl(String shortUri, HttpServletRequest request, HttpServletResponse response) throws IOException {
        //0.  获取短链接全路径
        String serverName = request.getServerName();
        String serverPort = Optional.of(request.getServerPort())
                .filter(each -> !Objects.equals(each, 80))
                .map(String::valueOf)
                .map(each ->":" + each)
                .orElse("");
        String fullShortUrl = serverName + serverPort + "/" + shortUri;

        // 1. 判断是否存在短链接缓存
        boolean contains = shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl);
        // 1.1 布隆过滤器不存在该短链接
        if(!contains){
            response.sendRedirect("/page/notfound");
            return;
        }
        // 2 误判情况 查看该短链接是否为空值
        String gotoIsNullShortlink = redisTemplate.opsForValue().get(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl));
        // 2.1 是空
        if(StrUtil.isNotBlank(gotoIsNullShortlink)){
            response.sendRedirect("/page/notfound");
            return;
        }
        // 3. 查缓存 查看缓存中是否存在该短链接的缓存
        String originUrl = redisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
        if(StrUtil.isBlank(originUrl)){
            // 3.1 不存在， 存在缓存击穿风险需要进行加锁
            RLock lock = redissonClient.getLock(String.format(LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl));
            lock.lock();
            try{
                // 3.1.1 双检判定，防止重复查询数据库 有效或者失效  查是否存在缓存，再查是否为空值缓存
                originUrl = redisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
                if(StrUtil.isNotBlank(originUrl)){
                    shortLinkStats(buildLinkStatsRecordAndSetUser(fullShortUrl, request, response));
                    response.sendRedirect(originUrl);
                }
                gotoIsNullShortlink = redisTemplate.opsForValue().get(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl));
                if(StrUtil.isNotBlank(gotoIsNullShortlink)){
                    response.sendRedirect("/page/notfound");
                    return;
                }

                // 3.1.2 查数据库 先查goto表获得分片键然后在查Link表
                LambdaQueryWrapper<LinkGotoDo> linkGotoWrapper = Wrappers.<LinkGotoDo>lambdaQuery()
                        .eq(LinkGotoDo::getFullShortUrl, fullShortUrl);
                LinkGotoDo linkGotoDo = linkGotoMapper.selectOne(linkGotoWrapper);
                if(linkGotoDo == null) {
                    // 缓存穿透 null 添加
                    redisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), "-", 30, TimeUnit.MINUTES);
                    response.sendRedirect("/page/notfound");
                    return;
                }
                LambdaQueryWrapper<LinkDo> queryWrapper = Wrappers.<LinkDo>lambdaQuery()
                        .eq(LinkDo::getGid, linkGotoDo.getGid())
                        .eq(LinkDo::getFullShortUrl, fullShortUrl)
                        .eq(LinkDo::getDelFlag, 0)
                        .eq(LinkDo::getEnableStatus, 0);
                LinkDo linkDo = baseMapper.selectOne(queryWrapper);

                if(linkDo == null || linkDo.getValidDate() != null && linkDo.getValidDate().before(new Date())){
                    // 缓存穿透 null 添加  连接已经过期
                    redisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), "-", 30, TimeUnit.MINUTES);
                    response.sendRedirect("/page/notfound");
                    return;
                }
                // 3.1.3 更新缓存
                redisTemplate.opsForValue()
                        .set(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
                                linkDo.getOriginUrl(),
                                LinkUtil.getLinkCacheValidDate(linkDo.getValidDate()),
                                TimeUnit.MICROSECONDS);
                shortLinkStats(buildLinkStatsRecordAndSetUser(fullShortUrl, request, response));
                response.sendRedirect(linkDo.getOriginUrl());
                return;
            }finally {
                lock.unlock();
            }
        }
        // 3.2 得到缓存
        shortLinkStats(buildLinkStatsRecordAndSetUser(fullShortUrl, request, response));
        response.sendRedirect(originUrl);
    }

    /**
     * 获取用户短链接访问统计 并设置用户id
     * @param fullShortUrl 短链接
     * @param request 请求
     * @param response 响应
     * @return
     */
    private ShortLinkStatsRecordDTO buildLinkStatsRecordAndSetUser(String fullShortUrl, HttpServletRequest request, HttpServletResponse response) {
        // 0. 改造成基于HyperLogLog的实现。 uv 初始化脚本
        DefaultRedisScript<Integer> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(UIP_UV_LUA_SCRIPT_PATH)));
        redisScript.setResultType(Integer.class);

        // 1. 设置用户uid
        AtomicBoolean uvFirstFlag = new AtomicBoolean();
        Cookie[] cookies = request.getCookies();
        AtomicReference<String> uv = new AtomicReference<>();
        // 异步添加cookie 和 缓存
        Runnable addResponseCookieTask = () -> {
            uv.set(UUID.fastUUID().toString());
            Cookie uvCookie = new Cookie("uv", uv.get());
            uvCookie.setMaxAge(60 * 60 * 24 * 30);
            uvCookie.setPath(StrUtil.sub(fullShortUrl, fullShortUrl.indexOf("/"), fullShortUrl.length()));
            response.addCookie(uvCookie);

            Integer ret = redisTemplate.execute(redisScript, Lists.newArrayList(SHORT_LINK_STATS_UV_KEY + fullShortUrl), Lists.newArrayList(uv.get()));

            uvFirstFlag.set(ret != null && ret > 0? Boolean.TRUE: Boolean.FALSE);
            // 旧版
//           redisTemplate.opsForValue().set(SHORT_LINK_STATS_UV_KEY + fullShortUrl + ":" +uv.get(), "-", 60 * 60 * 24 * 30, TimeUnit.MINUTES);
        };
        // 2. 判断并设置对应的UV 并判断是否为第一次访问
        if (ArrayUtil.isNotEmpty(cookies)) {
            Arrays.stream(cookies)
                    .filter(each -> Objects.equals(each.getName(), "uv"))
                    .findFirst()
//                    .map(Cookie::getValue)
                    .ifPresentOrElse(cookie -> {
                        String uvToken = cookie.getValue();
                        uv.set(uvToken);
                        // 改造成基于HyperLogLog的实现。 uv
                        Integer ret = redisTemplate.execute(redisScript, Lists.newArrayList(SHORT_LINK_STATS_UV_KEY + fullShortUrl), Lists.newArrayList(uv.get()));

                        uvFirstFlag.set(ret != null && ret > 0? Boolean.TRUE: Boolean.FALSE);
                        // 旧版
//                        Boolean uvAdded = redisTemplate.opsForValue().setIfAbsent(SHORT_LINK_STATS_UV_KEY + fullShortUrl + ":" + uvToken, "-", 60 * 60 * 24 * 30, TimeUnit.MINUTES);
//                        uvFirstFlag.set(Boolean.TRUE.equals(uvAdded));
                        cookie.setMaxAge(60 * 60 * 24 * 30);
                        response.addCookie(cookie); // 重置过期时间
                    }, addResponseCookieTask);
        } else {
            addResponseCookieTask.run();
        }
        // 3. 封装IP地址 浏览器、 设备 以及网络等状况
        String remoteAddr = LinkUtil.getActualIp(request);
        String os = LinkUtil.getOS(request);
        String browser = LinkUtil.getBrowser(request);
        String device = LinkUtil.getDevice((request));
        String network = LinkUtil.getNetwork(request);

        Integer ret = redisTemplate.execute(redisScript, Lists.newArrayList(SHORT_LINK_STATS_UIP_KEY + fullShortUrl), Lists.newArrayList(remoteAddr));
        boolean uipFirstFlag = ret != null && ret > 0;
//        Long uipAdded = redisTemplate.opsForSet().add(SHORT_LINK_STATS_UIP_KEY + fullShortUrl, remoteAddr);
//        boolean uipFirstFlag = uipAdded != null && uipAdded > 0L;
        return ShortLinkStatsRecordDTO.builder()
                .fullShortUrl(fullShortUrl)
                .uv(uv.get())
                .uvFirstFlag(uvFirstFlag.get())
                .uipFirstFlag(uipFirstFlag)
                .remoteAddr(remoteAddr)
                .os(os)
                .browser(browser)
                .device(device)
                .network(network)
                .currentDate(new Date())
                .build();
    }

    @Override
    public void shortLinkStats(ShortLinkStatsRecordDTO statsRecord) {
        // 1. 向消息队列中发送消息
        Map<String, String> producerMap = new HashMap<>();
        producerMap.put("statsRecord", JSON.toJSONString(statsRecord));
//        shortLinkStatsSaveProducer.send(producerMap);
        rabbitMqProducer.send(producerMap, MQConstans.PROJECT_EXCHANGE, MQConstans.STATS_SAVE_TOPIC);
    }

    @Override
    public ShortLinkBatchCreateRespDTO batchCreateShortLink(ShortLinkBatchCreateReqDTO requestParam) {
        List<String> originUrls = requestParam.getOriginUrls();
        List<String> describes = requestParam.getDescribes();
        List<ShortLinkBaseInfoRespDTO> result = new ArrayList<>();
        if(originUrls.size() > 10){
            throw new ClientException("演示环境，一次性仅支持创建10条以内数据");
        }
        for (int i = 0; i < originUrls.size(); i++) {
            ShortLinkCreateReqDTO shortLinkCreateReqDTO = BeanUtil.toBean(requestParam, ShortLinkCreateReqDTO.class);
            shortLinkCreateReqDTO.setOriginUrl(originUrls.get(i));
            shortLinkCreateReqDTO.setDescribe(describes.get(i));
            try {
                ILinkService obj = (ILinkService) AopContext.currentProxy();
                ShortLinkCreateRespDTO shortLink = obj.createShortLink(shortLinkCreateReqDTO);
                ShortLinkBaseInfoRespDTO linkBaseInfoRespDTO = ShortLinkBaseInfoRespDTO.builder()
                        .fullShortUrl(shortLink.getFullShortUrl())
                        .originUrl(shortLink.getOriginUrl())
                        .describe(describes.get(i))
                        .build();
                result.add(linkBaseInfoRespDTO);
            } catch (Throwable ex) {
                log.error("批量创建短链接失败，原始参数：{}", originUrls.get(i));
            }
        }
        return ShortLinkBatchCreateRespDTO.builder()
                .total(result.size())
                .baseLinkInfos(result)
                .build();
    }

    @Override
    public void removeLinkByGid(ShortLinkRemoveByGidDTO requestParam) {
        // 1. 获取并校验gid是否属于该用户
        checkGroupBelongToUser(requestParam.getGid());
        LambdaUpdateWrapper<LinkDo> updateWrapper = Wrappers.<LinkDo>lambdaUpdate()
                .eq(LinkDo::getGid, requestParam.getGid())
                .eq(LinkDo::getDelFlag, 0);
        // 操作db
        LinkDo delShortLinkDO = LinkDo.builder()
                .delTime(System.currentTimeMillis())
                .enableStatus(1)
                .build();
        delShortLinkDO.setDelFlag(1);
        baseMapper.update(delShortLinkDO, updateWrapper);
    }

    public void checkGroupBelongToUser(String gid) throws ServiceException {
        // 获取用户
        String username = Optional.ofNullable(UserContext.getUsername())
                .orElseThrow(() -> new ServiceException("用户未登录"));
        // 操作db判断是否属于该用户
        LambdaQueryWrapper<GroupDo> queryWrapper = Wrappers.lambdaQuery(GroupDo.class)
                .eq(GroupDo::getGid, gid)
                .eq(GroupDo::getUsername, username);
        List<GroupDo> groupDOList = linkGroupMapper.selectList(queryWrapper);
        if (CollUtil.isEmpty(groupDOList)) {
            throw new ServiceException("用户信息与分组标识不匹配");
        }
    }

    private String generateSuffix(ShortLinkCreateReqDTO requestParam){
        // 1. 获取参数
        String originUrl = requestParam.getOriginUrl();
        int customGenerateCount = 0;
        String shortLink = null;
        // 2. 生成短链接
        while(true){
            // 2.1 最大生成次数为10
            if(customGenerateCount > 10){
                throw new ClientException("短链接生成失败，请稍后再试");
            }
            // 2.2 获取随机的
            shortLink = HashUtil.hashToBase62(originUrl+ UUID.randomUUID());
            if(shortUriCreateCachePenetrationBloomFilter.contains(createShortLinkDefaultDomain +"/"+shortLink)){
                ++customGenerateCount;
            }else{
                break;
            }
        }
        return shortLink;
    }

    /**
     * 获取指定url的favicon
     * @param uri
     * @return
     */
    @SneakyThrows
    private void getFavicon(String uri) {
        URL baseUrl = new URL(uri);
        String host = baseUrl.getHost();
        String favicon = redisTemplate.opsForValue().get(SHORT_LINK_FAVICON_KEY+host);
        if(StrUtil.isNotBlank(favicon)){
            return;
        }


        Document doc = Jsoup.connect(uri).get();
        Elements links = doc.select("link[href~=.*\\.(ico|png)]");
        String faviconUrl = null;
        for (Element link : links) {
            if (link.attr("href").toLowerCase().contains("favicon")) {
                faviconUrl = link.absUrl("href");
                break;
            }
        }

        if (faviconUrl == null) {
            // Fallback to default favicon location
            faviconUrl = baseUrl.getProtocol() + "://" + host + "/favicon.ico";
        }
        redisTemplate.opsForValue().set(SHORT_LINK_FAVICON_KEY+host, faviconUrl);
    }

    private void verificationWhitelist(String originUrl) {
        // 1. 判断白名单是否开启
        Boolean enable = gotoDomainWhiteListConfiguration.getEnable();
        if (enable == null || !enable) {
            return;
        }
        // 2. 解析当前连接域名
        String domain = LinkUtil.extractDomain(originUrl);
        if (StrUtil.isBlank(domain)) {
            throw new ClientException("跳转链接填写错误");
        }
        // 3. 判断是否在白名单中
        List<String> details = gotoDomainWhiteListConfiguration.getDetails();
        if (!details.contains(domain)) {
            throw new ClientException("演示环境为避免恶意攻击，请生成以下网站跳转链接：" + gotoDomainWhiteListConfiguration.getNames());
        }
    }

    /**
     * 用于对比测试布隆过滤器场景，这是加分布式锁创建短链接的代码
     * @param requestParam
     * @return
     */
    private String generateSuffixByLock(ShortLinkCreateReqDTO requestParam) {
        int customGenerateCount = 0;
        String shorUri;
        while (true) {
            if (customGenerateCount > 10) {
                throw new ServiceException("短链接频繁生成，请稍后再试");
            }
            String originUrl = requestParam.getOriginUrl();
            originUrl += UUID.randomUUID().toString();
            shorUri = HashUtil.hashToBase62(originUrl);
            LambdaQueryWrapper<LinkDo> queryWrapper = Wrappers.lambdaQuery(LinkDo.class)
                    .eq(LinkDo::getGid, requestParam.getGid())
                    .eq(LinkDo::getFullShortUrl, createShortLinkDefaultDomain + "/" + shorUri)
                    .eq(LinkDo::getDelFlag, 0);
            LinkDo shortLinkDO = baseMapper.selectOne(queryWrapper);
            if (shortLinkDO == null) {
                break;
            }
            customGenerateCount++;
        }
        return shorUri;
    }
}
